﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using ALC.Core.Camera;
using ALC.Core.Constants;
using ALC.Core.Enums;
using ALC.Core.Helpers;
using ALC.Core.ImageProcessing;
using ALC.Core.Misc;
using ALC.Core.ViewModels.Chart;
using ALC.Core.ViewModels.GraphicPack;
using ALC.Core.ViewModels.Message;
using ALC.Core.ViewModels.Table;
using ALC.Core.ViewModels.Workpiece;
using CodeScan;
using HalconDotNet;
using KeyenceCl3000;
using PLCCommunication.Core;
using Precitec;
using WPFCommon.Commands;
using WPFCommon.Helpers;
using WPFCommon.Interface;
using WPFCommon.Logger;
using WPFCommon.ViewModels.Base;
using WPFCommon.ViewModels.Misc;
using Timer = System.Timers.Timer;

namespace ALC.Core.ViewModels.Application
{
    public class ApplicationViewModel : ViewModelBase
    {
        #region private field

        private static readonly string[] FaiNames = {"Pt1", "Pt2", "Pt3", "Pt4", "Pt5", "Pt6"};
        private CHR2Controller _precitectController;
        private IList<IFlushAll> _flushables = new List<IFlushAll>();

        // TODO: define ip address
        private string _precitecControllerIp;

        // TODO: define signal ids
        private int[] _signalIds;

        private static ApplicationViewModel _instance;
        private AlcServerViewModel _server;
        private Dictionary<CavityType, IList<FaiItem>> _faiItemsDict;
        private ICodeScanner _scanner1;
        private ICodeScanner _scanner2;

        private SimpleCsvLogger _csvLogger = new SimpleCsvLogger()
            {HeaderLine = string.Join(",", FaiNames), LogPath = Path.Combine(DirectoryConstants.LogDir, "Data.csv")};

        private SimpleCsvLogger _rawCsvLogger = new SimpleCsvLogger()
            {HeaderLine = string.Join(",", FaiNames), LogPath = Path.Combine(DirectoryConstants.LogDir, "RawData.csv")};

        private ProfilerViewModel _balanceProfiler;
        private ProfilerViewModel _measureProfiler;

        private IList<HCamera> _cameras;

        #endregion

        public static ApplicationViewModel Instance => _instance;

        #region ctor

        public ApplicationViewModel()
        {
            // Load application configs and start auto-serialization
            ApplicationConfigViewModel.Instance =
                AutoSerializableHelper.LoadAutoSerializable<ApplicationConfigViewModel>(
                    DirectoryConstants.ConfigDir, "AppConfigs");
            ApplicationConfigViewModel.Instance.ShouldAutoSerialize = true;

            SetupProfilers();
            LoadFaiItems();
            InitCommands();
        }

        private void InitCommands()
        {
            DoSimulationCommand = new RelayCommand(DoSimulation);
        }

        private Timer _simulationTimer;

        private void DoSimulation()
        {
            TestMessageQueueView();
        }

        private void LoadFaiItems()
        {
            FaiItemsCavity1 =
                AutoSerializableHelper.LoadAutoSerializables<FaiItem>(FaiNames,
                    Path.Combine(DirectoryConstants.ConfigDir, "Cavity1"));
            FaiItemsCavity2 =
                AutoSerializableHelper.LoadAutoSerializables<FaiItem>(FaiNames,
                    Path.Combine(DirectoryConstants.ConfigDir, "Cavity2"));

            _faiItemsDict = new Dictionary<CavityType, IList<FaiItem>>()
            {
                [CavityType.Cavity1] = FaiItemsCavity1,
                [CavityType.Cavity2] = FaiItemsCavity2
            };
        }

        private void SetupProfilers()
        {
            var logger = new DataLogger(Path.Combine(DirectoryConstants.LogDir, "MeasureProfile.txt"))
                {BufferSize = 10};
            _measureProfiler = new ProfilerViewModel("测量") {DataLogger = logger};
            _flushables.Add(logger);

            logger = new DataLogger(Path.Combine(DirectoryConstants.LogDir, "BalanceProfile.txt")) {BufferSize = 10};
            _balanceProfiler = new ProfilerViewModel("调平") {DataLogger = logger};
            _flushables.Add(logger);

            ProfilerViewModels = new List<ProfilerViewModel>() {_measureProfiler, _balanceProfiler};
        }

        #endregion


        #region props
        public KeyenceCl3000Controller KeyenceController { get; set; }

        public IList<LineChartViewModel> KeyenceLineChartViewModels { get; set; }
        
        public LineChartViewModel LineChartViewModel { get; set; }

        public IList<GraphicPackViewModel> GraphicPacks { get; set; }
        
        public IList<ProfilerViewModel> ProfilerViewModels { get; set; }

        public WorkpieceTableListViewModel WorkpieceTableListViewModel { get; set; } =
            new WorkpieceTableListViewModel(20);

        public bool OutputRawData { get; set; } = false;

        public string Code1OfWaitingWorkpiece { get; private set; }


        public Dictionary<string, double[]> SignalCurvesData { get; set; }

        // TODO: define signal names
        public string[] SignalNames { get; set; }
        public double AngleThreshold { get; set; }

        private IList<FaiItem> FaiItemsCavity1 { get; set; }
        private IList<FaiItem> FaiItemsCavity2 { get; set; }

        public WorkpieceDictionaryViewModel WorkpieceDictViewModel { get; } = new WorkpieceDictionaryViewModel();

        public ICommand DoSimulationCommand { get; set; }

        #endregion


        #region api

        public static void Init()
        {
            _instance = new ApplicationViewModel();
        }
        
        public static void Cleanup()
        {
            // TODO: disconnect lasers
            
            Instance._server.Disconnect();
            
            // Flush remaining logs
            foreach (var flushable in Instance._flushables)
            {
                flushable.FlushAll();
            }
            ImageCollector.CloseCameras();
        }

        public static void SetupHardware()
        {
            _instance.SetupServer();
            _instance.SetupBalancingModule();
            _instance.SetupThicknessMeasuringModule();

            _instance.SetupCameras();
        }

        private void SetupCameras()
        {
            ImageCollector.Init();
            ImageCollector.Instance.FirstImageReceived += ScanCode2;
            ImageCollector.Instance.ImagesCollectionDone += OnImagesCollected;
        }

        private void OnImagesCollected(IList<HImage> images)
        {
            // Locate
            var pointParams = Calculation.Locate(images);
            var message = new PlcMessagePack()
            {
                MsgType = PlcMessagePack.RespondIndicator,
                CommandId = CommandConstants.CommandId_LoactingStart,
                Text = $"point1({pointParams[0]},{pointParams[1]})_point2({pointParams[2]},{pointParams[3]})"
            };
            // TODO: what's channel 1 and 2?
            _server.SentToPlc(message);
            
            // TODO: Update ui
           
        }

        private void ScanCode2()
        {
            WorkpieceDictViewModel.CurrentWorkpiece.Code2 = _scanner2.Scan();
        }

        #endregion


        #region impl
        
 

        private void SetupThicknessMeasuringModule()
        {
            // Setup precitec controller
            _precitectController = new CHR2Controller(_precitecControllerIp, _signalIds, SignalNames);
            _precitectController.CurvesUpdated += curves => SignalCurvesData = curves;
        }

        private void SetupBalancingModule()
        {
            IDataProvider[] dataProviders = 
            {
                new OutValueProvider(), new OutValueProvider(), new OutValueProvider(), new OutValueProvider(), 
            };
            KeyenceController = new KeyenceCl3000Controller(new []{(byte)192,(byte)168, (byte)1, (byte)101}, new RelayMessageLogger(message=>{Logger.LogToUi(message, message);}), dataProviders );
        }

        private void SetupServer()
        {
            // Setup custom command parser
            var commandParser = new PlcCommandParser();
            commandParser.PlcAskForAngles += CalcAngles;
            commandParser.ThicknessMeasureStart += StartMeasuringThickness;
            commandParser.WorkpieceEnter += OnWorkpieceEnter;
            commandParser.ScanCode1 += () => { Code1OfWaitingWorkpiece = _scanner1.Scan(); };

            // Setup error parser
            var plcErrorParser = new PlcErrorParser();
            plcErrorParser.WarningL1Emit += OnL1WarningReceived;
            plcErrorParser.WarningL2Emit += OnL2WarningReceived;
            plcErrorParser.WarningL3Emit += OnL3WarningReceived;
            plcErrorParser.WarningL4Emit += OnL4WarningReceived;

            //
            var logPath = Path.Combine(DirectoryConstants.LogDir, "PlcLog.txt");
            var plcLogger = new FileLogger(TimeSpan.FromDays(3), logPath, 1000);
            plcLogger.StartLogging();
            _flushables.Add(plcLogger);

            _server = new AlcServerViewModel(commandParser) {ErrorParser = plcErrorParser, FileLogger = plcLogger};
        }

        private void OnWorkpieceEnter(CavityType enterCavity)
        {
            // Generate new workpiece instance
            var workpiece = new WorkpieceViewModel()
            {
                Code1 = Code1OfWaitingWorkpiece,
                FaiItems = _faiItemsDict[enterCavity].Clone()
            };
            WorkpieceDictViewModel.SetActiveWorkpiece(enterCavity, workpiece);
        }


        private void OnL4WarningReceived(string arg1, long arg2)
        {
            throw new NotImplementedException();
        }

        private void OnL3WarningReceived(string arg1, long arg2)
        {
            throw new NotImplementedException();
        }

        private void OnL2WarningReceived(string arg1, long arg2)
        {
            throw new NotImplementedException();
        }

        private void OnL1WarningReceived(string arg1, long arg2)
        {
            throw new NotImplementedException();
        }

        private async void StartMeasuringThickness(int positionIndex)
        {
            var delay = ApplicationConfigViewModel.Instance.ThicknessMeasureDelay;
            if (delay > 1)
            {
                await Task.Delay(delay);
            }

            var thickness = _precitectController.GetLastSample()[0];
            float binNo = 0;
            switch (positionIndex)
            {
                case 1:
                    WorkpieceDictViewModel.CurrentWorkpiece.FaiItems[0].ValueUnbiased = thickness;
                    // Measured first point
                    Logger.LogToUi("测量第一个点", "1001");
                    // Start clocking
                    _measureProfiler.Restart();
                    break;
                case 2:
                    WorkpieceDictViewModel.CurrentWorkpiece.FaiItems[1].ValueUnbiased = thickness;
                    break;
                case 3:
                    WorkpieceDictViewModel.CurrentWorkpiece.FaiItems[2].ValueUnbiased = thickness;
                    break;
                case 4:
                    WorkpieceDictViewModel.CurrentWorkpiece.FaiItems[3].ValueUnbiased = thickness;
                    break;
                case 5:
                    WorkpieceDictViewModel.CurrentWorkpiece.FaiItems[4].ValueUnbiased = thickness;
                    break;
                case 6:
                    WorkpieceDictViewModel.CurrentWorkpiece.FaiItems[5].ValueUnbiased = thickness;
                    WorkpieceDictViewModel.CurrentWorkpiece.EvaluateResult();
                    binNo = WorkpieceDictViewModel.CurrentWorkpiece.Result.ToFloat();
                    // Measured last point
                    Logger.LogToUi("完成测量","1006");
                    Task.Run(LogDataToFile);
                    _measureProfiler.Stop();
                    break;
            }

            var messagePack = new PlcMessagePack()
                {CommandId = CommandConstants.CommandId_ThicknessMeasureOnePointDone, Param1 = binNo};
            _server.SentToPlc(messagePack);
        }

        private void LogDataToFile()
        {
            var logTime = DateTime.Now.ToString("MMdd-HHmm-ss");
            var data = WorkpieceDictViewModel.CurrentWorkpiece.FaiItems.Select(item => item.Value);
            _csvLogger.Log(data,
                new[]
                {
                    WorkpieceDictViewModel.CurrentWorkpiece.Result.ToString(),
                    WorkpieceDictViewModel.ActiveCavity.ToString(), logTime
                });

            if (!OutputRawData) return;
            var rawData = WorkpieceDictViewModel.CurrentWorkpiece.FaiItems.Select(item => item.ValueUnbiased);
            _csvLogger.Log(rawData,
                new[]
                {
                    WorkpieceDictViewModel.CurrentWorkpiece.Result.ToString(),
                    WorkpieceDictViewModel.ActiveCavity.ToString(), logTime
                });
        }

        private void CalcAngles(bool isFirstTimeCurrentWorkpiece)
        {
            // Start clocking for balancing if this is the first time to balance this workpiece
            if (isFirstTimeCurrentWorkpiece)
            {
                _balanceProfiler.Restart();
            }

            // Todo: handle invalid data
            var heights = KeyenceController.QueryOutItems().Select(item => item.Value).ToArray();
            // Todo: specify the following values
            double x1 = 0, y1 = 0, x2 = 0, y2 = 0, x3 = 0, y3 = 0;
            var angles = Calculation.GetAngleRes(x1, y1, heights[0], x2, y2, heights[1], x3, y3, heights[2]);

            var messagePack = new PlcMessagePack() {CommandId = CommandConstants.CommandId_BalancingFinishOnce};
            bool balancingOver = angles.All(angle => Math.Abs(angle) < AngleThreshold);
            // If balancing is over for this workpiece send 0, otherwise send 1
            messagePack.Param1 = balancingOver ? 0 : 1;
            // If balancing is over for this workpiece send 0, otherwise send angles
            messagePack.Param2 = (float) (balancingOver ? 0 : angles[0]);
            messagePack.Param3 = (float) (balancingOver ? 0 : angles[1]);

            if (balancingOver)
            {
                _balanceProfiler.Stop();
            }

            _server.SentToPlc(messagePack);
        }

        #endregion

        #region tests

        private void TestMessageQueueView()
        {
            Task.Run(() =>
            {
                Logger.LogToUi(DateTime.Now.ToString("ffff"), "test");
            });
        }

        private void TestKeyenceLineCharts()
        {
            KeyenceLineChartViewModels = new List<LineChartViewModel>()
            {
                new LineChartViewModel(100, 50, new DataProviderMock()){Title = "Test1"},
                new LineChartViewModel(100, 50, new DataProviderMock()){Title = "Test2"},
                new LineChartViewModel(100, 50, new DataProviderMock()){Title = "Test3"},
                new LineChartViewModel(100, 50, new DataProviderMock()){Title = "Test4"},
            };
        }

        private void TestLineChartViewModel()
        {
            LineChartViewModel = new LineChartViewModel(100, 50, new DataProviderMock()){Title = "Test"};
        }

        private void TestGraphicPackListView()
        {
            var random = new Random();

            var randIntGen = new Func<int>(() => random.Next(0, 150));
            var value = randIntGen() % 2;
            
            var image = new HImage(value == 1? "YuChan.png" : "TaNe.png");
            var graphicPack = new GraphicPackViewModel()
            {
                Image = image, Graphics = null
            };
            GraphicPacks = new List<GraphicPackViewModel>()
            {
                graphicPack, graphicPack, graphicPack
            };
        }

        private void TestProfilerView()
        {
            var random = new Random();

            var randIntGen = new Func<int>(() => random.Next(0, 150));
            _balanceProfiler.Restart();
            _measureProfiler.Restart();
            Thread.Sleep(randIntGen());
            _balanceProfiler.Stop();
            _measureProfiler.Stop();
        }

        private void TestTableView()
        {
            var random = new Random();
            var randIntGen = new Func<int>(() => random.Next(0, 150));
            var cavity = (CavityType) (randIntGen() % 2 + 1);
            var now = DateTime.Now.ToString("u");

            var faiItems = new List<FaiItem>()
            {
                new FaiItem()
                {
                    MaxBoundary = 100, MinBoundary = 50, Name = "pt1", ShouldAutoSerialize = false,
                    ValueUnbiased = randIntGen()
                },
                new FaiItem()
                {
                    MaxBoundary = 100, MinBoundary = 50, Name = "pt2", ShouldAutoSerialize = false,
                    ValueUnbiased = randIntGen()
                },
                new FaiItem()
                {
                    MaxBoundary = 100, MinBoundary = 50, Name = "pt3", ShouldAutoSerialize = false,
                    ValueUnbiased = randIntGen()
                },
                new FaiItem()
                {
                    MaxBoundary = 100, MinBoundary = 50, Name = "pt4", ShouldAutoSerialize = false,
                    ValueUnbiased = randIntGen()
                },
                new FaiItem()
                {
                    MaxBoundary = 100, MinBoundary = 50, Name = "pt5", ShouldAutoSerialize = false,
                    ValueUnbiased = randIntGen()
                },
                new FaiItem()
                {
                    MaxBoundary = 100, MinBoundary = 50, Name = "pt6", ShouldAutoSerialize = false,
                    ValueUnbiased = randIntGen()
                },
            };


            var workpiece = new WorkpieceViewModel()
            {
                Code1 = now, Code2 = now, FaiItems = faiItems, Cavity = cavity
            };
            WorkpieceDictViewModel.SetActiveWorkpiece(cavity, workpiece);

            WorkpieceTableListViewModel.Add(workpiece);
        }

        private void TestWorkpieceDictionaryView()
        {
            var random = new Random();
            var randIntGen = new Func<int>(() => random.Next(0, 150));
            var cavity = (CavityType) (randIntGen() % 2 + 1);
            var now = DateTime.Now.ToString("u");

            var faiItems = new List<FaiItem>()
            {
                new FaiItem() {MaxBoundary = 100, MinBoundary = 50, Name = "pt1", ShouldAutoSerialize = false},
                new FaiItem() {MaxBoundary = 100, MinBoundary = 50, Name = "pt2", ShouldAutoSerialize = false},
                new FaiItem() {MaxBoundary = 100, MinBoundary = 50, Name = "pt3", ShouldAutoSerialize = false},
                new FaiItem() {MaxBoundary = 100, MinBoundary = 50, Name = "pt4", ShouldAutoSerialize = false},
                new FaiItem() {MaxBoundary = 100, MinBoundary = 50, Name = "pt5", ShouldAutoSerialize = false},
                new FaiItem() {MaxBoundary = 100, MinBoundary = 50, Name = "pt6", ShouldAutoSerialize = false},
            };


            var workpiece = new WorkpieceViewModel()
            {
                Code1 = now, Code2 = now, FaiItems = faiItems, Cavity = cavity
            };
            WorkpieceDictViewModel.SetActiveWorkpiece(cavity, workpiece);

            // Assign random values to fai items
            _simulationTimer?.Dispose();
            _simulationTimer = new Timer(1000);
            _simulationTimer.Elapsed += (sender, args) =>
            {
                var index = randIntGen() % 6;
                var value = randIntGen();
                faiItems[index].ValueUnbiased = value;
            };

            _simulationTimer.Start();
        }

        #endregion
    }
}